C++11中的通用引用

您所在的位置:网站首页 c++ 右值引用 作用 C++11中的通用引用

C++11中的通用引用

2024-07-17 10:17| 来源: 网络整理| 查看: 265

通用引用在语法上很容易和右值引用混淆,所以本文介绍了构成通用引用条件。然后,着重介绍类型推导和引用折叠(reference collapsing)是如何演绎出通用引用的。

左值右值都可引用 (1)

熟悉右值引用的话,就会知道它是通过&&来声明的。可能看见&&的第一反应就是右值引用。可惜,有些情况下竟然不是。在C++11中的右值引用一篇中,我们看到了std::move的源代码:

12345template typename remove_reference::type&& move(T&& t) noexcept{ return static_cast(t);}

这里std::move的参数t,就不是右值引用。若是右值引用的话,你怎么能传一个左值给它当参数呢?它的作用可是把左值转换成右值啊。所以,t必须能匹配左值。另一方面,这样的代码也不会出错:

1234Foo f("123");Foo&& r1 = std::move(std::move(f));Foo&& r2 = std::move(Foo("456"));

也就是说,t也能够匹配右值。实际上,参数t就是一个通用引用。通用引用,可以使用左值表达式初始化,也可以使用右值表达式初始化。匹配左值表达式时,它表现的像一个左值引用;匹配右值表达式时,它表现的像一个右值引用。

构成通用引用的条件 (2)

出现&&的时候,如何判断它是右值引用,还是通用引用呢?通用引用有两个条件:

形如T&&; T的类型需要推导;

先看一些例子:

12345678910111213141516int lvalue = 3;auto&& r1 = lvalue;auto&& r2 = 4;int&& r3 = 5;auto&& r4 = r3; //等价于int& r4 = r3;std::vector vect;auto&& r5 = vect[0];templatevoid func(T&& p);func(lvalue);func(10);

这里r3是右值引用,因为没有类型推导;相反,r1, r2, r4, r5和p都是通用引用:

r1等价于左值引用; r2等价于右值引用; r4等价于左值引用;没错,是左值引用,因为r3是左值(虽然它是右值引用类型的变量); r5等价于左值引用;因为std::vector::operator[]返回左值引用(左值引用是左值); func(lvalue)中,p等价于左值引用; func(10)中,p等价于右值引用;

也就是说,形如T&&的类型中,若T是已知类型,则是右值引用;若T需要推导,则是通用引用。这两个条件看起来不难,但实际上还是比看起来要苛刻:

例1:必须形如T&&

12345678int lvalue = 3;const auto&& r = lvalue; //错误:r不是通用引用,而是右值引用,故不能通过左值初始化!templatevoid func(const T&& p);func(lvalue); //错误:p不是通用引用,而是右值引用,故不能通过左值初始化!

注:必须是T&&,const T&&都不行。当然,T可以是auto或者别的类型参数名;

例2:必须存在类型推导

12345678templateclass Bar{ private: T _m; public: Bar(Bar&& other); //不是通用引用,而是右值引用};

虽然是类模板,但没有类型推导,other的类型就是Bar&&,所以也不是通用引用。甚至下面的也不是:

123456789101112131415templateclass Bar{ private: T _m; public: Bar(T&& t); //不是通用引用,而是右值引用};template class vector{public: void push_back(T&& x); //不是通用引用,而是右值引用 };

这里也不存在类型推导:Bar()和push_back()不可能脱离它们的class而独立存在,然而,当它们的class产生的时候(也即是,类模板的实例化的时候),类型参数已经确定了。所以,调用Bar()和push_back()的时候,不存在类型推导。

而下面这种情况,却是通用引用,因为调用Bar()和emplace_back()的时候,需要推导类型:

1234567891011121314151617templateclass Bar{ private: T _m; public: template Bar(S&& s); // s是通用引用};template class vector{public: template void emplace_back(Args&&... args); // args中的每一个参数都是是通用引用};

例3:推导的必须是T&&中的T的类型

12templatevoid func(std::vector&& p);

虽然这里存在类型推导,但推导是std::vector的类型参数,而不是T&&中的T(实际上,p的类型是已知的std::vector),所以,p也不是通用引用。

通用引用类型的变量 (3)

一个通用引用类型的变量,不管它是通过左值初始化的,还是通过右值初始化的,它本身是一个左值。这类似于**右值引用类型的变量是左值(因为有名字)**,见C++11中的右值引用。

如果它引用的是左值(通过左值初始化的),我们当然想把它当作左值处理;相反,如果它引用的是右值(通过右值初始化的),我们又想保留它右值的特征(例如,能作为移动拷贝的参数)。你拿到一个通用引用类型的左值,它引用的可能是左值,也可能是右值,你不清楚(例如,你在写一个摸板函数,它的参数是通用引用,你当然不知道调用者传进左值还是右值),但你还想保留它原有特征(左值或右值),怎么办呢?std::forward可以帮你,见C++11中的完美转发。

类型推导和引用折叠 (4)

前面我们看到,通用引用在引用左值时变成左值引用,在引用右值时变成右值引用。这种奇特的效果是如何产生的呢?

编译器允许它匹配左值和右值; 类型推导和引用折叠(reference collapsing)最终让它变成左值引用或者右值引用; 类型推导规则 (4.1)

首先,通用引用auto&&和T&&中的类型推导规则是这样的:

若匹配的是左值lv,auto或T被推导成lv的引用类型;例如匹配i时(int i=3;),推导结果是int&; 若匹配的是右值rv,auto或T被推导成rv的类型;例如匹配3时,推导结果是int; 引用的引用是非法的 (4.2)

其次,必须说明,引用的引用是非法的,例如:

123456int f = 3;int & & r1 = f; //error: cannot declare reference to ‘int&’, which is not a typedef or a template type argumentint & && r2 = f; //error: cannot declare reference to ‘int&’, which is not a typedef or a template type argumentint && & r3 = f; //error: cannot declare reference to ‘int&&’, which is not a typedef or a template type argumentint && && r4 = f; //error: cannot declare reference to ‘int&&’, which is not a typedef or a template type argument

编译错误指出,不能声明int&或者int&&的引用(即引用的引用)。

编译过程中产生引用的引用 (4.3)

虽然我们不能在源代码里写引用的引用(如4.2节所示),但是由于第4.1节的类型推导规则(下面例1和例2),模板类型参数实例化(下面例3)和typedef展开(下面例4)等原因,在编译的过程中,可能产生引用的引用这种情况:

例1:

1234567templatevoid func(T&& p);func(3);int i=3;func(i);

根据类型推导规则:

对于func(3),T的推导结果是int,模板实例化的结果是void func(int&& p); 对于func(i),T的推导结果是int&,模板实例化的结果是void func(int& && p);

前一种情况,通用引用变成右值引用;后一种情况,模板实例化的过程中产生了引用的引用。

例2:

1234auto&& r1 = 3;int i = 3;auto&& r2 = i;

根据类型推导规则:

r1引用的是右值,所以auto的推导结果是int,r1的类型是int&&; r2引用的是左值,所以auto的推导结果是int&,r2的类型是int& &&;

通用引用r1变成右值引用;r2的类型推导过程中出现了引用的引用。

例3:

12345templateclass Bar { public: typedef T& LVRefType;};

使用引用类型(左值引用或右值引用)去实例化Bar:

12345int i=1;int j=2;Bar::LVRefType ri = i;Bar::LVRefType rj = j;

这样,模板类实例化过程中也产生引用的引用:

12typedef int& & LVRefType; //Bartypedef int&& & LVRefType; //Bar

例4:

123void func(Bar::LVRefType&& p);void func(Bar::LVRefType&& p);void func(Bar::LVRefType&& p);

因为Bar::LVRefType,Bar::LVRefType和Bar::LVRefType都等价于int&,所以,typedef展开得到的结果都是:

1void func(int& && p);

即,产生了引用的引用。

如第4.2节所说,引用的引用是非法的。那我们这些例子中都存在引用的引用怎么办呢?好在,这些引用的引用不是存在于源代码中(否则编译失败),而是在编译过程中临时产生的。编译器会立即消除它们,手段就是**引用折叠(reference collapsing)**。

引用折叠规则 (4.4)

左值引用和右值引用可以组合出四种引用的引用:

左值引用的左值引用; 左值引用的右值引用; 右值引用的左值引用; 右值引用的右值引用;

折叠(collapsing rules)规则比较简单:

右值引用的右值引用折叠成(collapses into)右值引用; 其他情况折叠成(collapses into)左值引用;

基于折叠规则,看看上面的四个例子:

例1:对于func(i),T的推导结果是int&,模板实例化过程中产生void func(int& && p),最终折叠成void func(int& p),也就是说,通用引用p最终变成左值引用; 例2:r2引用的是左值,所以auto的推导结果是int&,r2的类型展开成int& &&,最终折叠成int&,也就是说,通用引用r2最终变成左值引用; 例3:int& &和int&& &都折叠成int&,LVRefType最终变成int&;这么定义是正确的:无论取代T的是不是引用,是左值引用还是右值引用,LVRefType最终都是左值引用类型,和它的名字相符; 例4:int& && p被折叠成int& p;

可见,类型推导和引用折叠在通用引用机制中起着重要作用:编译器允许T&&匹配左值和右值,然后经过类型推导和(或)引用折叠,T&&才最终变成左值引用或者右值引用。

std::move的工作原理 (4.5)

回头看std::move的实现,你发现,它的工作原理不过是类型推导和引用折叠的另一个例子:

12345template typename remove_reference::type&& move(T&& t) noexcept{ return static_cast(t);}

move左值:

12Foo f;std::move(f);

根据类型推导规则(第4.1节),std::move的类型参数T推导为Foo&,std::move展开为:

1234typename remove_reference::type&& move(Foo& && t) noexcept{ return static_cast(t);}

经过remove_reference和引用折叠:

1234Foo&& move(Foo& t) noexcept{ return static_cast(t);}

可见:左值经过std::move()变为右值;

move右值:

1std::move(Foo("xyz"));

根据类型推导规则(第4.1节),std::move的类型参数T推导为Foo,std::move展开为:

1234typename remove_reference::type&& move(Foo&& t) noexcept{ return static_cast(t);}

经过remove_reference:

1234Foo&& move(Foo&& t) noexcept{ return static_cast(t);}

可见:右值经过std::move()还是右值;

引用剥除 (4.6)

其实,引用剥除(reference stripping)和引用折叠(reference collapsing)没有一毛钱关系。之所以在这里提它,纯粹是为了消除直观上的混淆。

在C++11中的auto关键字中,我们已经见到过引用剥除,auto类型推导的时候,需要进行引用剥除,模板类型参数推导也需要。

12345678910templatevoid func(T&& p);int&& r1 = 3;int x = 3;int& r2 = x; func(r1);func(r2); 问题:在func(r1)中p的类型是int&& &&,然后折叠成int&&,对吗?答:不对。在推导类型T的时候,r1的右值引用是被剥除的(int&& r1=3中的&&被剥除)。又因为r1是一个左值(说过很多次了,右值引用类型的变量有名字,因此是左值,见C++11中的右值引用),所以T推导为int&。因此,模板实例化为void func(int& && p),最终折叠成void func(int& p)。 问题:在func(r2)中,p的类型是int& &&,然后折叠成int&,对吗?答:对,但是,这和int& r2=x中的&无关,因为它也被剥除了。T被推导成int&完全是因为r2是左值的缘故。

可见,由于引用剥除的缘故,r1和r2声明中的&&和&起不到任何作用;模板参数类型的推导,完全取决于函数实参是左值还是右值。

小结 (5)

通用引用既能引用左值又能引用右值。实质上,是类型推导和引用折叠(reference collapsing)导致这种现象产生的。我们知道,const类型的左值引用(const T&)也能够同时引用左值和右值,那通用引用的优势是什么呢?见C++11中的完美转发。



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3